{ "cells": [ { "cell_type": "markdown", "id": "3f40677e", "metadata": {}, "source": [ "# High-Frequency Grid Trading - Simplified from GLFT\n", "\n", "One of the challenges in using the GLFT model is that it assumes a parametric form for order arrival intensity. However, in some cases, the data does not fit this function well. To address this, you can replace the parametric model with non-parametric approaches, such as using a simple mean, median, or a percentile estimate. A simpler solution is to remove the dynamic estimation of spread and skew based on order arrival intensity, relying only on volatility.\n", "\n", "This issue is more common in pairs with a large tick size. As demonstrated in [the large tick size assets example](https://hftbacktest.readthedocs.io/en/latest/tutorials/Queue-Based%20Market%20Making%20in%20Large%20Tick%20Size%20Assets.html), the problem can be mitigated by incorporating the micro price. While the BBO-based micro price has little impact on pairs with small tick sizes, it can be highly effective for large tick size pairs." ] }, { "cell_type": "code", "execution_count": 1, "id": "0c8ef816-5277-45f8-a46c-cdf43ae16989", "metadata": {}, "outputs": [], "source": [ "import json\n", "import datetime\n", "import itertools\n", "\n", "from multiprocessing import Pool\n", "\n", "import polars as pl\n", "\n", "import numpy as np\n", "\n", "from numba import njit, uint64, float64\n", "from numba.typed import Dict\n", "\n", "from matplotlib import pyplot as plt\n", "\n", "from hftbacktest import BUY, SELL, GTX, LIMIT, BUY_EVENT, SELL_EVENT\n", "from hftbacktest import BacktestAsset, ROIVectorMarketDepthBacktest, Recorder\n", "from hftbacktest.stats import LinearAssetRecord\n", "\n", "@njit\n", "def gridtrading(hbt, recorder, vol_to_half_spread, min_grid_step, grid_num, skew, max_notional_position):\n", " asset_no = 0\n", " tick_size = hbt.depth(asset_no).tick_size\n", " lot_size = hbt.depth(asset_no).lot_size\n", "\n", " mid_price_chg = np.full(300_000_000, np.nan, np.float64)\n", "\n", " t = 0\n", " prev_mid_price_tick = np.nan\n", " mid_price_tick = np.nan\n", " volatility = np.nan\n", " \n", " # Running interval in nanoseconds.\n", " while hbt.elapse(100_000_000) == 0:\n", " # Clears cancelled, filled or expired orders. \n", " hbt.clear_inactive_orders(asset_no)\n", "\n", " depth = hbt.depth(asset_no)\n", " position = hbt.position(asset_no)\n", " orders = hbt.orders(asset_no)\n", "\n", " best_bid = depth.best_bid\n", " best_ask = depth.best_ask\n", " best_bid_qty = depth.best_bid_qty\n", " best_ask_qty = depth.best_ask_qty\n", "\n", " micro_price = (best_bid * best_ask_qty + best_ask * best_bid_qty) / (best_bid_qty + best_ask_qty)\n", " mid_price = (best_bid + best_ask) / 2.0\n", "\n", " mid_price_tick = mid_price / tick_size\n", " \n", " # Records the mid-price change for volatility calculation.\n", " mid_price_chg[t] = mid_price_tick - prev_mid_price_tick\n", " prev_mid_price_tick = mid_price_tick\n", " \n", " #--------------------------------------------------------\n", " # Calculates the market volatility.\n", " \n", " # Updates the volatility every 5-sec.\n", " if t % 50 == 0:\n", " # Window size is 10-minute.\n", " if t >= 6_000 - 1:\n", " # Updates the volatility.\n", " volatility = np.nanstd(mid_price_chg[t + 1 - 6_000:t + 1]) * np.sqrt(10)\n", "\n", " notional_position = position * mid_price\n", " normalized_position = notional_position / max_notional_position\n", "\n", " half_spread_tick = volatility * vol_to_half_spread\n", "\n", " bid_depth_tick = half_spread_tick * (1 + skew * normalized_position)\n", " ask_depth_tick = half_spread_tick * (1 - skew * normalized_position)\n", "\n", " # Please see Market Making with Alpha example series.\n", " # https://hftbacktest.readthedocs.io/en/latest/tutorials/Market%20Making%20with%20Alpha%20-%20Order%20Book%20Imbalance.html\n", " # https://hftbacktest.readthedocs.io/en/latest/tutorials/Market%20Making%20with%20Alpha%20-%20Basis.html\n", " # https://hftbacktest.readthedocs.io/en/latest/tutorials/Market%20Making%20with%20Alpha%20-%20APT.html\n", " #\n", " # Without alpha, this relies heavily on rebates combined with short-term mean reversion to the current price — \n", " # a behavior that has been observed to be particularly strong in altcoins.\n", " forecast_mid_price = micro_price # mid_price + b1 * alpha\n", "\n", " # Sets the order quantity to be equivalent to a notional value of $100.\n", " order_qty = max(round((100 / mid_price) / lot_size), 1) * lot_size\n", "\n", " # Since our price is skewed, it may cross the spread. To ensure market making and avoid crossing the spread, \n", " # limit the price to the best bid and best ask.\n", " bid_price = np.minimum(forecast_mid_price - bid_depth_tick * tick_size, best_bid)\n", " ask_price = np.maximum(forecast_mid_price + ask_depth_tick * tick_size, best_ask)\n", "\n", " # min_grid_step enforces grid interval changes to be no less than min_grid_step, which\n", " # stabilizes the grid_interval and keeps the orders on the grid more stable.\n", " grid_interval = max(np.round(half_spread_tick * tick_size / min_grid_step) * min_grid_step, min_grid_step)\n", "\n", " # Aligns the prices to the grid.\n", " bid_price = np.floor(bid_price / grid_interval) * grid_interval\n", " ask_price = np.ceil(ask_price / grid_interval) * grid_interval\n", " \n", " #--------------------------------------------------------\n", " # Updates quotes.\n", " \n", " # Creates a new grid for buy orders.\n", " new_bid_orders = Dict.empty(np.uint64, np.float64)\n", " if normalized_position < 1 and np.isfinite(bid_price):\n", " for i in range(grid_num):\n", " bid_price_tick = round(bid_price / tick_size)\n", " \n", " # order price in tick is used as order id.\n", " new_bid_orders[uint64(bid_price_tick)] = bid_price\n", " \n", " bid_price -= grid_interval\n", "\n", " # Creates a new grid for sell orders.\n", " new_ask_orders = Dict.empty(np.uint64, np.float64)\n", " if normalized_position > -1 and np.isfinite(ask_price):\n", " for i in range(grid_num):\n", " ask_price_tick = round(ask_price / tick_size)\n", " \n", " # order price in tick is used as order id.\n", " new_ask_orders[uint64(ask_price_tick)] = ask_price\n", "\n", " ask_price += grid_interval\n", " \n", " order_values = orders.values();\n", " while order_values.has_next():\n", " order = order_values.get()\n", " # Cancels if a working order is not in the new grid.\n", " if order.cancellable:\n", " if (\n", " (order.side == BUY and order.order_id not in new_bid_orders)\n", " or (order.side == SELL and order.order_id not in new_ask_orders)\n", " ):\n", " hbt.cancel(asset_no, order.order_id, False)\n", " \n", " for order_id, order_price in new_bid_orders.items():\n", " # Posts a new buy order if there is no working order at the price on the new grid.\n", " if order_id not in orders:\n", " hbt.submit_buy_order(asset_no, order_id, order_price, order_qty, GTX, LIMIT, False)\n", " \n", " for order_id, order_price in new_ask_orders.items():\n", " # Posts a new sell order if there is no working order at the price on the new grid.\n", " if order_id not in orders:\n", " hbt.submit_sell_order(asset_no, order_id, order_price, order_qty, GTX, LIMIT, False)\n", " \n", " #--------------------------------------------------------\n", " # Records variables and stats for analysis.\n", " \n", " t += 1\n", "\n", " if t >= len(mid_price_chg):\n", " raise Exception\n", " \n", " # Records the current state for stat calculation.\n", " recorder.record(hbt)" ] }, { "cell_type": "markdown", "id": "4dc02bfd-1dd2-475d-a029-8d9e2ed5ea56", "metadata": {}, "source": [ "## Binance\n", "\n", "